﻿using System;
using System.Collections.Generic;
using System.IO;
using System.Net.Sockets;
using System.Threading;

namespace Nutilities.Net.Binary
{
    public abstract class ResponseConnection<T> : ThreadedConnection<T>
    {
        public delegate void ResponseReceivedEvent(ResponseHeader response);
        public event ResponseReceivedEvent ResponseReceived;

        private readonly Dictionary<T, ResponseHeader> _responses = new Dictionary<T, ResponseHeader>(); 
        private readonly Dictionary<T, AutoResetEvent> _responseEvents = new Dictionary<T, AutoResetEvent>();

        public int ResponseTimeout { get; set; }
        protected T ResponseCommand { get; set; }

        protected ResponseConnection(Socket socket)
            : base(socket)
        {
            ResponseTimeout = -1;
        }

        protected ResponseConnection(ResponseConnection<T> connection)
            : base(connection)
        {
            ResponseTimeout = -1;
        }

        protected virtual void OnResponseReceived(ResponseHeader response)
        {
            if (_responseEvents.ContainsKey(response.Command))
            {
                _responses[response.Command] = response;

                // Signal the AutoResetEvent, to continue WaitForResponse
                _responseEvents[response.Command].Set();

                // Remove the reference to the AutoResetEvent
                _responseEvents.Remove(response.Command);
            }
            if (ResponseReceived != null)
                ResponseReceived(response);
        }

        protected virtual ResponseHeader WaitForResponse(T command)
        {
            return WaitForResponse(command, ResponseTimeout);
        }

        protected virtual ResponseHeader WaitForResponse(T command, int millisecondsTimeout)
        {
            // Calling this method within the OnCommandReceived method will cause a deadlock,
            // so an exceptino is thrown if that's the case.
            if (Thread.CurrentThread == Thread)
            {
                throw new ThreadStateException("This method cannot be called from within the reading thread.");
            }

            // Only create a new object when one isn't referenced in the array,
            // in order to prevent one thread hanging if this method is called in two threads at the same time
            if (!_responseEvents.ContainsKey(command))
            {
                _responseEvents[command] = new AutoResetEvent(false);
            }

            // Wait for a response to be read from the input stream
            bool result = _responseEvents[command].WaitOne(millisecondsTimeout);

            if (!result)
            {
                throw new TimeoutException("ResponseHeader wasn't received in time.");
            }

            return _responses[command];
        }

        protected virtual void HandleResponse()
        {
            // Read the response data
            ResponseHeader response = new ResponseHeader
            {
                Command = ReadCommand(),
                Success = Reader.ReadBoolean()
            };

            if (!response.Success)
            {
                // Only attempt to read a string if an error is expected
                response.Error = Reader.ReadString();
            }

            // Signal the ones waiting for the response
            OnResponseReceived(response);
        }

        protected override void OnCommandReceived(T command)
        {
            if (command.Equals(ResponseCommand))
            {
                HandleResponse();
            }
            else
            {
                base.OnCommandReceived(command);
            }
        }

        public class ResponseHeader
        {
            public T Command { get; set; }
            public bool Success { get; set; }
            public string Error { get; set; }
        }
    }
}
